Migration Guide: From Legacynet to HyperBEAM
Migrating your process to HyperBEAM is primarily about exposing your process state via HTTP. This single change provides the biggest performance improvement and is the core of the HyperBEAM experience.
The Migration is Simple: Expose Process State via HTTP
The only real step to migrate your process to HyperBEAM is exposing your process state via HTTP.
Your existing process logic, handlers, and message handling work exactly the same. The difference is that instead of forcing clients to use slow dry-run messages to read your state, you expose it via HTTP for instant access.
Before (Legacynet Pattern)
-- Slow: Clients must send dry-run messages to read state
Handlers.add(
"GetBalance",
Handlers.utils.hasMatchingTag("Action", "GetBalance"),
function(msg)
local balance = Balances[msg.Tags.Target] or 0
return msg.reply({ Data = tostring(balance) })
end
)
-- Client side (slow and expensive)
const result = await dryrun({
process: processId,
tags: [{ name: "Action", value: "GetBalance" }]
});After (HyperBEAM Pattern)
-- Fast: Expose state via HTTP for instant access
Handlers.add(
"Transfer",
Handlers.utils.hasMatchingTag("Action", "Transfer"),
function(msg)
-- Your existing transfer logic
Balances[msg.From] = tostring(tonumber(Balances[msg.From]) - amount)
Balances[msg.Tags.Target] = tostring(tonumber(Balances[msg.Tags.Target]) + amount)
-- NEW: Expose updated balances via HTTP
Send({
device = 'patch@1.0',
balances = Balances
})
end
)
-- Client side (instant and free)
const response = await fetch(`https://forward.computer/${processId}~process@1.0/compute/balances`);
const balances = await response.json();Step-by-Step Migration
Step 1: Connect to HyperBEAM
Change your connection:
# From Legacynet
aos
# To HyperBEAM
aos --node https://forward.computerThat's it for the connection - your process works the same way.
Step 2: Add Initial State Sync
Make your existing state available immediately:
-- Add this to the top of your process
Balances = Balances or { alice = 1000, bob = 500 }
TotalSupply = TotalSupply or 1500
-- Initial sync - runs once when process loads
InitialSync = InitialSync or 'INCOMPLETE'
if InitialSync == 'INCOMPLETE' then
Send({
device = 'patch@1.0',
balances = Balances,
totalsupply = tostring(TotalSupply)
})
InitialSync = 'COMPLETE'
endStep 3: Update Your Handlers to Expose State
For each handler that modifies state, add a Send call to expose the updated state:
-- Example: Token transfer handler
Handlers.add(
"Transfer",
Handlers.utils.hasMatchingTag("Action", "Transfer"),
function(msg)
-- Your existing transfer logic (unchanged)
local amount = tonumber(msg.Tags.Amount)
local target = msg.Tags.Target
-- Validate and update balances (your existing logic)
local senderBalance = tonumber(Balances[msg.From]) or 0
if senderBalance < amount then
return msg.reply({ Error = "Insufficient balance" })
end
Balances[msg.From] = tostring(senderBalance - amount)
Balances[target] = tostring((tonumber(Balances[target]) or 0) + amount)
-- NEW: Expose updated state via HTTP
Send({
device = 'patch@1.0',
balances = Balances,
totalsupply = tostring(TotalSupply)
})
return msg.reply({ Status = "Success" })
end
)Step 4: Update Your Frontend
Replace dry-run calls with HTTP requests:
// Old way (slow and expensive)
async function getBalance(address) {
const result = await dryrun({
process: processId,
tags: [
{ name: "Action", value: "GetBalance" },
{ name: "Target", value: address },
],
});
return result.Messages[0].Data;
}
// New way (instant and free)
async function getBalance(address) {
const response = await fetch(
`https://forward.computer/${processId}~process@1.0/compute/balances`,
);
const balances = await response.json();
return balances[address] || "0";
}Common Migration Patterns
Token Process
-- Add state exposure to your existing token handlers
Handlers.add(
"Mint",
Handlers.utils.hasMatchingTag("Action", "Mint"),
function(msg)
-- Your existing mint logic
Balances[msg.Tags.Target] = tostring(tonumber(Balances[msg.Tags.Target]) + tonumber(msg.Tags.Amount))
TotalSupply = TotalSupply + tonumber(msg.Tags.Amount)
-- Expose updated state
Send({
device = 'patch@1.0',
messages = Messages,
messageCount = tostring(#Messages)
})
end
)Chat Process
-- Add state exposure to your existing chat handlers
Handlers.add(
"AddMessage",
Handlers.utils.hasMatchingTag("Action", "AddMessage"),
function(msg)
-- Your existing message logic
table.insert(Messages, {
user = msg.From,
content = msg.Data,
timestamp = os.time()
})
-- Expose updated messages
Send({
device = 'patch@1.0',
cache = {
messages = Messages,
messageCount = tostring(#Messages)
}
})
end
)Counter Process
-- Add state exposure to your existing counter handlers
Handlers.add(
"Increment",
Handlers.utils.hasMatchingTag("Action", "Increment"),
function(msg)
-- Your existing counter logic
Counter = Counter + 1
-- Expose updated counter
Send({
device = 'patch@1.0',
counter = Counter,
lastUpdate = os.time()
})
end
)That's It!
Your process is now migrated to HyperBEAM.
The key benefits you now have:
- Instant state reads via HTTP instead of slow dry-runs
- Better performance for web frontends and data services
- Lower costs (no more expensive dry-run messages)
- Simpler client code (standard HTTP requests)
Advanced (Optional)
Once you're comfortable with basic state exposure, you can explore:
- Dynamic Reads - Compute values on-the-fly
- State Exposure - Patterns for marketplace applications
- HyperBEAM Devices - Advanced extensibility
But the core migration is complete - you've successfully moved your process to HyperBEAM by adding state exposure!